
package com.bangcommunity.bbframe.springredis;

import com.bangcommunity.bbframe.common.base.MainConfigLoader;
import com.bangcommunity.bbframe.common.tuple.MutablePair;
import com.bangcommunity.bbframe.common.tuple.Pair;
import com.bangcommunity.bbframe.common.utils.lang.CollectionUtils;
import com.bangcommunity.bbframe.common.utils.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.util.JedisURIHelper;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Created by tanghc on 17/1/13.
 *
 * 获得项目相关的redis客户端。 建议在使用cache的地方调用 QtProjectRedisClient.getSpringRedisCache()获取
 * 这样可以保持最新的客户端(若redis配置信息修改了,会重新生成客户端) 否则始终保持一个客户端必须修改redis必须重启
 *
 */
public class ProjectRedisClientBuilder {

    private final static Logger logger = LoggerFactory.getLogger(ProjectRedisClientBuilder.class);
    private static ConcurrentHashMap<ProjectRedisClientConf, ISpringRedisCache> clientMap = new ConcurrentHashMap<>();
    private static final String productCode = MainConfigLoader.getInstance().getProductCode();
    private static final String appCode = MainConfigLoader.getInstance().getAppCode();

    private ProjectRedisClientBuilder() {
    }

    private static ISpringRedisCache createISpringRedisCache(ProjectRedisClientConf projectRedisClientConf) {
        JedisConnectionFactory jedisConnectionFactory = buildNewJedisConnectionFactory(projectRedisClientConf);
        if (null != jedisConnectionFactory) {
            logger.warn("modify redis info create new redisclient:productCode={},appCode={}", productCode, appCode);
            return buildNewRedisRepository(jedisConnectionFactory, projectRedisClientConf);
        } else {
            logger.warn(
                    "didnot modify redis info create new redisclient no jedisconnection factory :productCode={},appCode={}",
                    productCode, appCode);
        }
        return null;
    }

    private static String getProjectPrefix(String productCode, String appCode) {

        return new StringBuilder().append(productCode).append(":").append(appCode).toString();
    }

    /**
     * always return new
     *
     * @return
     */
    public static JedisConnectionFactory buildNewJedisConnectionFactory(ProjectRedisClientConf projectRedisClientConf) {

        boolean usePool = projectRedisClientConf.getUsePool();

        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();

        JedisShardInfo jedisShardInfo = buildJedisShardInfo(projectRedisClientConf);
        RedisSentinelConfiguration redisSentinelConfiguration = buildRedisSentinelConfiguration(projectRedisClientConf);
        RedisClusterConfiguration redisClusterConfiguration = buildRedisClusterConfiguration(projectRedisClientConf);
        if (null != redisSentinelConfiguration) {
            jedisConnectionFactory = new JedisConnectionFactory(redisSentinelConfiguration);
            jedisConnectionFactory.setShardInfo(jedisShardInfo);
        }
        if (null != redisClusterConfiguration) {
            jedisConnectionFactory = new JedisConnectionFactory(redisClusterConfiguration);
        }
        if (null != jedisShardInfo) {
            jedisConnectionFactory.setShardInfo(jedisShardInfo);
        }
        if (usePool) {
            JedisPoolConfig jedisPoolConfig = buildJedisPoolConfig(projectRedisClientConf);
            jedisConnectionFactory.setPoolConfig(jedisPoolConfig);
        }
        if (null != jedisConnectionFactory) {
            String passwd = projectRedisClientConf.getPassword();
            Integer timeout = projectRedisClientConf.getTimeout();
            if (StringUtils.isNotBlank(passwd)) {
                jedisConnectionFactory.setPassword(passwd);
            }
            if (null != timeout) {
                jedisConnectionFactory.setTimeout(timeout);
            }
            String dbIndex = projectRedisClientConf.getDb();
            if (StringUtils.isNotBlank(dbIndex)) {
                jedisConnectionFactory.setDatabase(Integer.parseInt(dbIndex));
            }
            // important
            jedisConnectionFactory.afterPropertiesSet();
        }
        return jedisConnectionFactory;
    }

    private static JedisShardInfo buildJedisShardInfo(ProjectRedisClientConf projectRedisClientConf) {
        String url = projectRedisClientConf.getUrl();
        JedisShardInfo jedisShardInfo = null;
        String passwd = projectRedisClientConf.getPassword();
        if (StringUtils.isBlank(url)) {
            String host = projectRedisClientConf.getHost();
            if (StringUtils.isNotBlank(host)) {
                Integer portStr = projectRedisClientConf.getPort();
                String dbStr = projectRedisClientConf.getDb();
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.append("http://");
                if (StringUtils.isNotBlank(passwd)) {
                    stringBuilder.append("password:").append(passwd).append("@");
                }
                if (StringUtils.isBlank(dbStr)) {
                    dbStr = "0";
                }
                stringBuilder.append(host).append(":").append(portStr).append("/").append(dbStr);
                url = stringBuilder.toString();
            }
        }
        if (StringUtils.isNotBlank(url)) {
            jedisShardInfo = new JedisShardInfo(url);
        }

        logger.warn("redis  set shard info: {}", url);
        return jedisShardInfo;
    }

    private static RedisClusterConfiguration buildRedisClusterConfiguration(
            ProjectRedisClientConf projectRedisClientConf) {
        String cluster = projectRedisClientConf.getClusterNodes();
        if (StringUtils.isNotBlank(cluster)) {

            int maxRedirects = projectRedisClientConf.getClusterMaxRedirects();
            Set<String> hostAndPortsSet = getHostAndPortsSet(cluster);
            if (CollectionUtils.isNotEmpty(hostAndPortsSet)) {
                RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(hostAndPortsSet);
                clusterConfiguration.setMaxRedirects(maxRedirects);
                logger.warn("redis use cluster factory and set info: {}", cluster);
                return clusterConfiguration;
            }
        }
        return null;
    }

    private static RedisSentinelConfiguration buildRedisSentinelConfiguration(
            ProjectRedisClientConf projectRedisClientConf) {
        String sentinel = StringUtils.trimAllWhitespace(projectRedisClientConf.getSentinelUrl());
        if (StringUtils.isNotBlank(sentinel)) {
            Set<String> sentinelHostAndPorts = getHostAndPortsSet(sentinel);
            if (CollectionUtils.isNotEmpty(sentinelHostAndPorts)) {
                RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(
                        projectRedisClientConf.getSentinelMasterName(), sentinelHostAndPorts);
                logger.warn("redis use sentinel factory and set info: {}", sentinel);
                return sentinelConfig;
            }
        }
        return null;
    }

    /**
     * 解析地址中host:port集合
     * 
     * @param rawUrl
     * @return
     */
    private static Set<String> getHostAndPortsSet(String rawUrl) {
        rawUrl = StringUtils.trimAllWhitespace(rawUrl);
        if (StringUtils.isNotBlank(rawUrl)) {
            List<Pair<String, Integer>> hostPorts = parseHostPorts(rawUrl);
            Set<String> sentinelHostAndPorts = new HashSet<>();
            if (null != hostPorts) {
                for (Pair<String, Integer> pair : hostPorts) {
                    sentinelHostAndPorts.add(pair.getLeft() + ":" + pair.getRight());
                }
            }
            return sentinelHostAndPorts;
        }
        return null;
    }

    private static JedisPoolConfig buildJedisPoolConfig(ProjectRedisClientConf projectRedisClientConf) {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(projectRedisClientConf.getMaxIdle());
        jedisPoolConfig.setMaxTotal(projectRedisClientConf.getMaxTotal());
        jedisPoolConfig.setMaxWaitMillis(projectRedisClientConf.getMaxWaitMillis());
        jedisPoolConfig.setMinEvictableIdleTimeMillis(projectRedisClientConf.getMinEvictableIdleTimeMillis());
        jedisPoolConfig.setMinIdle(projectRedisClientConf.getMinIdle());
        jedisPoolConfig.setNumTestsPerEvictionRun(projectRedisClientConf.getNumTestsPerEvictionRun());
        jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(projectRedisClientConf.getMinEvictableIdleTimeMillis());
        jedisPoolConfig.setTestOnBorrow(projectRedisClientConf.getTestOnBorrow());
        jedisPoolConfig.setTestOnReturn(projectRedisClientConf.getTestOnReturn());
        jedisPoolConfig.setTestWhileIdle(projectRedisClientConf.getTestWhileIdle());
        jedisPoolConfig.setTimeBetweenEvictionRunsMillis(projectRedisClientConf.getTimeBetweenEvictionRunsMillis());
        return jedisPoolConfig;
    }


    /**
     * always return new
     *
     * @return
     */
    public static ISpringRedisCache buildNewRedisRepository(JedisConnectionFactory jedisConnectionFactory,
            ProjectRedisClientConf projectRedisClientConf) {

        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);

        // StringRedisSerializer
        StringRedisSerializer serializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(serializer);
        redisTemplate.setHashKeySerializer(serializer);
        // redisTemplate.setHashValueSerializer(serializer);

        // important
        redisTemplate.afterPropertiesSet();

        RedisRepository redisRepository = new RedisRepository();
        redisRepository.setRedisTemplate(redisTemplate);
        boolean useNameSpace = projectRedisClientConf.getUseNameSpace();
        if (useNameSpace) {
            redisRepository.setPrefix(getProjectPrefix(productCode, appCode));
        }

        return redisRepository;
    }

    /**
     * 原地址可以是 protol://host:port或者是host:port。多个使用","或者";"隔开 解析地址成 host:port 格式
     * 
     * @param rawUrl
     * @return
     */
    private static List<Pair<String, Integer>> parseHostPorts(String rawUrl) {
        String[] split = null;
        if (rawUrl.indexOf(",") != -1) {
            split = rawUrl.split(",");
        } else {
            split = rawUrl.split(";");
        }
        List<Pair<String, Integer>> hostPorts = new ArrayList<>();
        if (null != split && split.length > 0) {
            for (String s : split) {
                if (s.indexOf("://") == -1) {
                    s = "http://" + s;
                }
                try {
                    URI uri = URI.create(s);
                    if (JedisURIHelper.isValid(uri)) {
                        hostPorts.add(new MutablePair<>(uri.getHost(), uri.getPort()));
                    }
                } catch (Exception e) {
                    logger.warn("parse redis sentinel config invalid url: {},{}", s, e.getMessage());
                }
            }
        }
        return hostPorts;
    }


    public static <V> ISpringRedisCache<V> getSpringRedisCache(ProjectRedisClientConf projectRedisClientConf) {
        ISpringRedisCache iSpringRedisCache = clientMap.get(projectRedisClientConf);
        if(null == iSpringRedisCache){
            iSpringRedisCache = createISpringRedisCache(projectRedisClientConf);
            if(null != iSpringRedisCache){
                clientMap.put(projectRedisClientConf,iSpringRedisCache);
            }
        }
        return iSpringRedisCache;
    }
}
